﻿using System;
using System.Configuration;
using System.Linq;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Conventions;
using MongoDB.Driver;
using System.Collections.Generic;

namespace Charisma.MongoDB
{
    public sealed class MongoDbProvider : IDbProvider
    {
        private static readonly HashSet<Type> configuredContexts = new HashSet<Type>();

        #region IDbProvider Members

        public IDatabase GetOrCreateDatabase(ConnectionStringSettings connectionSettings)
        {
            try {
                var mongoUrl = MongoUrl.Create(connectionSettings.ConnectionString);
                var server = MongoServer.Create(mongoUrl);
                var mongoDatabase = server.GetDatabase(mongoUrl.DatabaseName);
                return new Database(this, mongoDatabase);
            }
            catch (Exception e) {
                throw TranslateException("Error getting or creating database, see inner exception for details.", e);
            }
        }

        public void DropDatabase(ConnectionStringSettings connectionSettings)
        {
            try {
                var mongoUrl = MongoUrl.Create(connectionSettings.ConnectionString);
                var server = MongoServer.Create(mongoUrl);
                server.DropDatabase(mongoUrl.DatabaseName);
            }
            catch (Exception e) {
                throw TranslateException("Error dropping database, see inner exception for details.", e);
            }
        }

        public CharismaException TranslateException(string message, Exception exception)
        {
            return new CharismaException(message, exception);
        }

        public void Initialize(DbModel dbModel)
        {
            if (configuredContexts.Contains(dbModel.DbContextType))
                return;

            var contextAssembly = dbModel.DbContextType.Assembly;
            var types = Enumerable.Repeat(contextAssembly, 1)
                .Concat(contextAssembly.GetReferencing())
                .SelectMany(a => a.GetTypes())
                .ToList();
            var repositories = dbModel.Repositories
                .Select(r => new {Repository = r, Types = types.Where(t => r.RootElement.Type.IsAssignableFrom(t))});
            foreach (var repositoryTypes in repositories)
            {
                var repoTypes = new HashSet<Type>(repositoryTypes.Types);
                var repository = repositoryTypes.Repository;
                var convention = new ConventionProfile();
                convention.SetIdGeneratorConvention(
                    new IdGeneratorConvention(repoTypes, repository.KeyGeneratorKind));
                BsonClassMap.RegisterConventions(convention, repoTypes.Contains);
                foreach (var type in repoTypes) {
                    if (BsonClassMap.IsClassMapRegistered(type))
                        continue;
                    var tc = dbModel.Entities.Find(type);
                    var mi = ReflectionService.GetGenericMethod(GetType(), "ConfigureClassMap", type);
                    mi.Invoke(this, new[] {tc});
                }
            }
            configuredContexts.Add(dbModel.DbContextType);
        }

        #endregion

        private void ConfigureClassMap<T>(EntityConfiguration tc = null)
        {
            var m = BsonClassMap.RegisterClassMap<T>();
            m.AutoMap();
            m.SetIgnoreExtraElements(true);

            if (tc == null)
                return;

            foreach (var fc in tc.Members) {

                if (m.ClassType != fc.Field.DeclaringType)
                    continue;

                if (fc.IsIgnored) {
                    m.UnmapMember(fc.Field);
                    continue;
                }

                string name = fc.Alias;
                if (string.IsNullOrEmpty(name))
                    name = fc.Name;

                var mm = m.GetMemberMap(name);
                if (mm == null) {
                    m.MapMember(fc.Field);
                    mm = m.GetMemberMap(fc.Field.Name);
                }

                if (mm.ElementName != name)
                    mm.SetElementName(name);
                
                if (fc.IsKey) {
                    m.SetIdMember(mm);
                    if (mm.ElementName != name)
                        fc.Alias = mm.ElementName;
                }
            }
        }
    }
}